﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ERF;
using ERF.Physics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using FirstPerson.Objects.Pickups;
using FirstPerson.Objects.Weapons;
using FirstPerson.States;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Audio;

namespace FirstPerson.Objects
{
    /// <summary>
    /// The AI class
    /// </summary>
    class AIPlayer : Player
    {
        private AudioEmitter audioEmitter;
        private SoundEffect soundEffect;

        // Fyzzy weights
        private float FuzzyDistanceWeight = 10f;
        private float fuzzy = 0.0f;

        private float MinDistance = 0.0f;
        private float MaxDistance = 60.0f;
        private int currentState = 0;
        private int previousState = 0;
        private TimeSpan timeInCurrentState = TimeSpan.Zero;
        private TimeSpan MinTime = TimeSpan.Zero;
        private TimeSpan MaxTime = TimeSpan.FromSeconds(10.0f);
        private int bestBehaviourState = 0;

        private WayPoint goToWaypoint;
        private WayPoint goalWaypoint;
        private Stack<WayPoint> pathList;
        private AstarPathfinding pathFinding;
        private Random random;
        private WayPoint[] waypoints;
      
        private HumanPlayer player;

        private const int HUNTING_STATE = 1;
        private const int ROAMING_STATE = 2;
        private const float MOVEMENT_FORCE = 600f;
        private const float JUMP_FORCE = 4000f;

        public WayPoint[] Waypoints
        {
            get { return waypoints; }
            set { waypoints = value; }
        }

        // Keeps track of the list of states that we can have.
        private List<int> behaviourStates;
        /// <summary>
        /// Creates a new instance of the AI
        /// </summary>
        /// <param name="game">The game this instance belongs to.</param>
        /// <param name="wps">The array of waypoints.</param>
        /// <param name="random"></param>
        public AIPlayer(ERFGame game, WayPoint[] wps, Random random)
            : base(game, "face_01_final")
        {
            this.game = game;
            base.scale = new Vector3(0.2011f, 0.2011f, 0.2011f);
            this.random = random;

            this.behaviourStates = new List<int>(2);
            this.behaviourStates.Add(HUNTING_STATE);
            this.behaviourStates.Add(ROAMING_STATE);

            this.waypoints = wps;
            this.pathFinding = new AstarPathfinding();
            SetNewPathAndPosition(wps);
          
            this.player = ((FirstPerson.PlayerHandler)game.PlayerHandler).HumanPlayer;

            this.soundEffect = game.Content.Load<SoundEffect>("Sounds\\Ghost");          
        }

        /// <summary>
        /// This function can be used as a respawn for the ghosts. it selects a random waypoint.
        /// makes a path and moves to it, also moves the position to the new startingpoint
        /// </summary>
        /// <param name="wps"></param>
        private void SetNewPathAndPosition(WayPoint[] wps)
        {
            this.goToWaypoint = GetRandomWaypoint();
            this.goalWaypoint = GetRandomWaypoint();
            this.pathList = pathFinding.GetPath(wps, goToWaypoint, goalWaypoint);
            this.position = new Vector3(goToWaypoint.Position.X, 0, goToWaypoint.Position.Y);
            this.pathList.Pop();
            base.PhysicsBody.position = position;
        }

     
        /// <summary>
        /// Finds and returns the waypoint that is closest to the player
        /// </summary>
        /// <returns></returns>
        private WayPoint FindWaypointClosestToPlayer()
        {
            float minDist = 10000;
            float dist = 0;
            WayPoint w = null;
            foreach (WayPoint wp in waypoints)
            {
                dist = DistanceToTarget(new Vector2(player.PhysicsBody.position.X, player.PhysicsBody.position.Z),
                    wp.Position);
                if (dist < minDist)
                {
                    w = wp;
                    minDist = dist;
                }
            }

            return w;
        }

        protected override void SetEffect()
        {
            for (int i = 0; i < this.ObjectModel.Meshes.Count; i++)
            {
                for (int j = 0; j < this.ObjectModel.Meshes[i].MeshParts.Count; j++)
                {
                    BasicShaderEffect effect;

                    if (this.ObjectModel.Meshes[i].MeshParts[j].Effect is BasicEffect)
                    {
                        BasicEffect oldEffect = (BasicEffect)this.ObjectModel.Meshes[i].MeshParts[j].Effect;
                        effect = new BasicShaderEffect(base.CurrentGame);
                        effect.DiffuseColor = Color.White.ToVector3();
                        effect.LightDirection = new Vector3(0, 1, 1);
                        effect.AmbientColor = Color.White.ToVector3();
                        effect.AmbientIntensity = 0.1f;
                        effect.DiffuseIntensity = 1f;
                        effect.SpecularColor = Color.White.ToVector3();
                        effect.SpecularIntensity = 1f;
                        effect.SpecularPower = 16f;
                        effect.EnableLight = true;
                        effect.FogColor = Color.White.ToVector4();
                        effect.FogStart = 10.1f;
                        effect.FogEnd = 10f;
                        effect.FogEnabled = false;
                    }
                    else
                    {
                        BasicShaderEffect oldEffect = (BasicShaderEffect)this.ObjectModel.Meshes[i].MeshParts[j].Effect;
                        effect = new BasicShaderEffect(base.CurrentGame);
                        effect.DiffuseColor = Color.White.ToVector3();
                        effect.LightDirection = new Vector3(0, 1, 1);
                        effect.AmbientColor = Color.White.ToVector3();
                        effect.AmbientIntensity = 0.1f;
                        effect.DiffuseIntensity = 1f;
                        effect.SpecularColor = Color.White.ToVector3();
                        effect.SpecularIntensity = 1f;
                        effect.SpecularPower = 16f;
                        effect.EnableLight = true;
                        effect.FogColor = Color.White.ToVector4();
                        effect.FogStart = 10.1f;
                        effect.FogEnd = 10f;
                        effect.FogEnabled = false;
                    }

                    this.meshEffects[i][j] = effect;
                }
            }
            
            
        }

        private void SetNewPath()
        {

            if (currentState==ROAMING_STATE)
            {
                SetRoamingPath();
            }
            else
            {
                SetHuntingPath();
            }
        }

        protected override void body_BodyChanged(object sender, EventArgs e)
        {
            Vector3 feetPosition = this.body.position;
            feetPosition.Y -= this.body.boundingBox.Max.Y;
            this.position = feetPosition+new Vector3(0,1.5f,0);


         
        }

        private void SetHuntingPath()
        {
            WayPoint wp = FindWaypointClosestToPlayer();
            if (wp == goToWaypoint)
            {
                this.goalWaypoint = wp;
          
                this.pathList.Push(goToWaypoint);
            }
            else
            {
                this.goalWaypoint = wp;
                this.pathList = this.pathFinding.GetPath(this.waypoints,
                    this.goToWaypoint, goalWaypoint);
             
            }
        }

        private void SetRoamingPath()
        {
            goalWaypoint = GetRandomWaypoint();
            this.pathList = this.pathFinding.GetPath(this.waypoints,
                this.goToWaypoint, goalWaypoint);
      
        }
        /// <summary>
        /// Returns a random waypoint
        /// </summary>
        /// <returns></returns>
        private WayPoint GetRandomWaypoint()
        {
            return this.waypoints[this.random.Next(this.waypoints.Length)];
        }

        private Vector3 SetAttackingPath(Vector3 force)
        {
            //Move towards the player
            force = new Vector3(player.Position.X, this.position.Y, player.Position.Z) - this.position;
            if (force != Vector3.Zero)
                force.Normalize();               

            return force;
        }
        public override void Update(GameTime gameTime, ref Matrix world)
        {
            if (this.audioEmitter == null && (base.game.PlayerHandler as FirstPerson.PlayerHandler).HumanPlayer != null)
            {
                SoundEffectInstance instance = this.soundEffect.CreateInstance();
                this.audioEmitter = new AudioEmitter();
                instance.IsLooped = true;
    
                game.SoundHandler.Play3DSound(instance, this, (game.PlayerHandler as FirstPerson.PlayerHandler).HumanPlayer);
            }
            if (this.audioEmitter != null)
            {
                this.audioEmitter.Position = this.position;
                this.audioEmitter.Forward = this.world.Forward;
                this.audioEmitter.Up = Vector3.Up;
                this.audioEmitter.Velocity = this.body.velocity;
            }
            previousState = currentState;
            if (currentState == ROAMING_STATE)
            {
                PlayingState.GetInstance(this.CurrentGame.StateMachine).particleSystemGhost.AddParticle(this.position + this.world.Forward * 3, Vector3.Zero);
                PlayingState.GetInstance(this.CurrentGame.StateMachine).particleSystemGhost.AddParticle(this.position + this.world.Forward * 3, Vector3.Zero);
            }
            else
            {
                PlayingState.GetInstance(this.CurrentGame.StateMachine).particleSystemGhostAngry.AddParticle(new Vector3(position.X, 1.5f, position.Z) + this.world.Forward * 3, Vector3.Zero);
                PlayingState.GetInstance(this.CurrentGame.StateMachine).particleSystemGhostAngry.AddParticle(new Vector3(position.X, 1.5f, position.Z) + this.world.Forward * 3, Vector3.Zero);
            }
            player = ((FirstPerson.PlayerHandler)game.PlayerHandler).HumanPlayer;

            currentState = this.ChooseBehaviourState(gameTime);
            Vector3 force = Vector3.Zero;
            if (currentState==ROAMING_STATE || currentState==HUNTING_STATE)
            {
                //Just follow a waypoint path
                force = new Vector3(goToWaypoint.Position.X, this.position.Y, goToWaypoint.Position.Y) - this.position;
                if (force != Vector3.Zero)
                    force.Normalize();
            }
            else
                // attack and follow the player
                    force = SetAttackingPath(force);

            if (previousState == ROAMING_STATE && currentState == HUNTING_STATE)
            {
                pathList.Clear();
            }

            // Makes the AI stop at a waypoint if it is close enough to the player.
            float distanceToTarget = DistanceToTarget(new Vector2(position.X, position.Z), goToWaypoint.Position);
            if (distanceToTarget < 5f )
            {
                if (pathList.Count == 0)
                    SetNewPath();
              
                this.goToWaypoint = pathList.Pop();

            }
            float distanceToPlayer = DistanceToTarget(new Vector2(position.X, position.Z), new Vector2(player.Position.X, player.Position.Z));
            // Changes the position of the AI and reduces the players hp
            if (distanceToPlayer < 2f)
            {
                SetNewPathAndPosition(waypoints);
                player.TakeDamage(40,player);
            }

            // Going directly towards the player if AI is close anough.
            if (distanceToPlayer < 10f && distanceToPlayer >= 2f)
            {
                pathList.Clear();
                force = SetAttackingPath(force);
            }

            RotateModel();
            this.body.internalForce = force * MOVEMENT_FORCE;

            base.Update(gameTime, ref world);
        }
        /// <summary>
        /// Rotating the model so it is always facing the player
        /// </summary>
        private void RotateModel()
        {
            Vector2 diff = (new Vector2(this.PhysicsBody.position.X, this.PhysicsBody.position.Z)) -
                    (new Vector2(player.PhysicsBody.position.X, player.PhysicsBody.position.Z));
            float desiredAngle = (float)Math.Atan2(diff.X, diff.Y);
            this.rotation.Y = desiredAngle + (MathHelper.PiOver2 * 2);
        }
        /// <summary>
        /// Returns the distance between 2 Vector2
        /// </summary>
        /// <param name="position1"></param>
        /// <param name="position2"></param>
        /// <returns></returns>
        private float DistanceToTarget(Vector2 position1, Vector2 position2)
        {
            return Vector2.Distance(position1, position2);
        }

        /// <summary>
        /// ChooseState contains fuzzy logic calcuations that will make the player
        /// choose its next behaviourstate.
        /// </summary>
        private int ChooseBehaviourState(GameTime gameTime)
        {
            float bestFuzzyValue = 0.0f;

            for (int i = 0; i < behaviourStates.Count; i++)
            {
                // calculate the distance to the player.
                float distance = DistanceToTarget((new Vector2(this.PhysicsBody.position.X, this.PhysicsBody.position.Z)),
                    (new Vector2(player.PhysicsBody.position.X, player.PhysicsBody.position.Z))); 
                
                // This variable will store the fuzzy value for this player. The player
                // will prefer behaviourState with a fuzzy value that is close to 1. 
                // The fuzzy value is based on distance.
                fuzzy = 0.0f;

                // We use the distance to the opponent and add in the fuzzy distance
                // value. This is a value that ranges from 0 to 1, and increases
                // as the player gets closer to the opponent.
                
                fuzzy += CalculateFuzzyDistance(distance, behaviourStates[i]) * FuzzyDistanceWeight;

                if (fuzzy > bestFuzzyValue)
                {
                    bestBehaviourState = behaviourStates[i];
                    bestFuzzyValue = fuzzy;
                }
            }
            //Add time in state.
            if (currentState == bestBehaviourState)
            {
                timeInCurrentState += gameTime.ElapsedGameTime;
            }
            else
            {
                timeInCurrentState = TimeSpan.Zero;
            }

            return (bestBehaviourState);
        }

        /// <summary>
        /// Returns a value from 0 to 1 that represents how long the AI has been
        /// in a specific state. If the value is 0, the AI isn't currently in the
        /// state, or just changed to it. If the value is 1, the AI has been in this
        /// state for a long time.
        /// </summary>
        private float CalculateFuzzyTime(int i)
        {
            // to calcuate the fuzzy time value, we see how long we've
            // been in the current state.
            TimeSpan time = TimeSpan.Zero;
            if (behaviourStates[i] == currentState)
            {
                time = timeInCurrentState;
            }

            // fuzzyTime is a value ranging from 0 to 1. It will decrease
            // as the amount of time we have spent in the current state
            // increases.
            float fuzzyTime = (float)((time - MinTime).TotalSeconds
                / (MaxTime - MinTime).TotalSeconds);
            return (1 - MathHelper.Clamp(fuzzyTime, 0, 1.0f));
        }

        /// <summary>
        /// Calculates a value from 0 to 1 that represents how "close" a player 
        /// is to an opponent.
        /// </summary>
        private float CalculateFuzzyDistance(float distance, float state)
        {
            if (state == ROAMING_STATE)
            {
                return ((distance - MinDistance) / (MaxDistance - MinDistance));
            }
            else
            {
                return (1 - (distance - MinDistance) / (MaxDistance - MinDistance));
            }
            
        }
    }
}
